/* 
Scenic Graphics Library
Copyright (C) 2007 Jouni Tulkki

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USApackage scenic;
*/

package scenic;

import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Stack;

import scenic.filter.Filter;
import scenic.path.LineSegment;
import scenic.path.Path;
import scenic.path.PathBuilder;

/**
 * ScenicGraphics class offers an easy to use interface for creating scene
 * trees. Instead of creating scene graphs directly, Graphics class
 * has a more traditional method-based interface. The Graphics class
 * transforms the method calls into appropriate scene graphs. Paths can be
 * created using methods that create lines, curves, arcs and rectangles
 * These paths can then be either stroked,
 * filled or clipped to. Text can also be drawn using simple method calls.
 * The current affine transformation can easily be changed using different
 * methods which translate, scale or rotate the current transformation.
 * The Graphics object also has a stack which can be used to store and restore 
 * the state of the Graphics object.
 */
public class ScenicGraphics {
	private static class State implements Cloneable
	{
		public SceneContainer scene;
		public AffineTransform transform;
		public Font font;
		public ScenicColor strokeColor;
		public SceneNode strokeBrush;
		public ScenicColor fillColor;
		public SceneNode fillBrush;
		public double lineWidth;
		public int lineCap;
		public int lineJoin;
		public double miterLimit;
		public float[] lineDashLengths;
		public double lineDashPhase;
		public int fillRule;
		public double[] textPosition;
		public boolean usesFractionalFontMetrics;
		
		public State() {
			textPosition = new double[2];
			this.transform = new AffineTransform();
		}
		
		public Object clone() {
			try {
				State s = (State)super.clone();
				
				s.textPosition = (double[])textPosition.clone();
				s.transform = (AffineTransform)transform.clone();
				
				return s;
			} catch(CloneNotSupportedException e) {
				return null;
			}			
		}
		
		public void set(State state) {
			this.scene = state.scene;
			this.transform.setTransform(state.transform);
			this.font = state.font;
			this.strokeColor = state.strokeColor;
			this.fillColor = state.fillColor;
			this.fillBrush = state.fillBrush;
			this.lineWidth = state.lineWidth;
			this.lineCap = state.lineCap;
			this.lineJoin = state.lineJoin;
			this.miterLimit = state.miterLimit;
			this.lineDashLengths = state.lineDashLengths;
			this.lineDashPhase = state.lineDashPhase;
			this.fillRule = state.fillRule;
			this.textPosition = state.textPosition;
			this.usesFractionalFontMetrics = state.usesFractionalFontMetrics;
		}
	};

	private Stack<State> statePool = new Stack<State>();
	
	private Stack<State> stateStack;
	private State state;
	private PathBuilder path;
	
	/**
	 * Constructs a new Graphics object using the given SceneContainer as
	 * the root node. The transformation matrix of the root node is also
	 * given. This matrix is used to position hinted glyphs properly.
	 * 
	 * @param scene The root node into which graphics is drawn.
	 * @param transform The transformation matrix of the root node.
	 */
	public ScenicGraphics(SceneContainer scene, AffineTransform transform) {
		stateStack = new Stack<State>();
		state = newState();
		state.scene = scene;
		state.transform = (AffineTransform)transform.clone();
		state.font = new Font("SansSerif", Font.PLAIN, 12);
		state.strokeColor = new ScenicColor(0, 0, 0);
		state.fillColor = state.strokeColor;
		state.lineWidth = 1.0;
		state.lineCap = LineCapStyle.BUTT_CAP;
		state.lineJoin = LineJoinStyle.BEVEL_JOIN;
		state.miterLimit = 10.0;
		state.lineDashLengths = null;
		state.lineDashPhase = 0.0;
		state.fillRule = FillRule.ODD_WINDING;
		state.usesFractionalFontMetrics = false;
		path = new PathBuilder();
	}

	/**
	 * Constructs a new Graphics object using the given SceneContainer as
	 * the root node.
	 * 
	 * @param scene The root node into which graphics is drawn.
	 */
	public ScenicGraphics(SceneContainer scene) {
		this(scene, new AffineTransform());
	}

	/** 
	 * Construct a new ScenicGraphics object that inherits the
	 * state of the given ScenicGraphics object.
	 * 
	 * @param parent the parent object
	 */
	public ScenicGraphics(ScenicGraphics parent) {
		stateStack = new Stack<State>();
		state = (State)parent.state.clone();
		state.scene = new SceneContainer();
		parent.state.scene.add(state.scene);
		path = new PathBuilder();
	}
	
	private State newState() {
		if(!statePool.empty())
			return statePool.pop();
		return new State();
	}
	
	private State cloneState(State state) {
		State s = newState();
		
		s.set(state);
		return s;
	}
	
	/**
	 * Creates a copy of this graphics object.
	 * 
	 * @return the copy
	 */
	public ScenicGraphics create() {
		return new ScenicGraphics(this);
	}
	
	/**
	 * Clears the current scene.
	 */
	public void clear() {
		state.scene.clear();
	}
	
	/**
	 * Pushes the state of this object into the stack. The state includes
	 * the entire internal state of the Graphics object excluding the 
	 * current path.
	 */
	public void push() {
		stateStack.push(state);
		state = cloneState(state);
	}
	
	/**
	 * Pops the state from the stack.
	 */
	public void pop() {
		statePool.push(state);
		state = stateStack.pop();
	}
	
	/**
	 * Multiplies the current transformation matrix with the
	 * given affine transform.
	 * 
	 * @param m the affine transform
	 */
	public void transform(AffineTransform m) {
		transformImpl((AffineTransform)m.clone());
	}

	private void transformImpl(AffineTransform m) {
		SceneTransform st = new SceneTransform(m);
		
		state.scene.add(st);
		state.scene = st;
		state.transform.concatenate(m);
	}
	
	/**
	 * Translates the current transformation matrix by the given
	 * displacement.
	 * 
	 * @param dx x coordinate of the translation
	 * @param dy y coordinate of the translation
	 */
	public void translate(double dx, double dy) {
		transform(AffineTransform.getTranslateInstance(dx, dy));
	}
	
	/**
	 * Scales the current transformation matrix by the given
	 * amount.
	 * 
	 * @param x x coordinate of the scale factor
	 * @param y y coordinate of the scale factor
	 */
	public void scale(double x, double y) {
		transform(AffineTransform.getScaleInstance(x, y));
	}
	
	/**
	 * Rotates the current transformation by the given angle.
	 * 
	 * @param angle rotation in degrees.
	 */
	public void rotate(double angle) {
		transform(AffineTransform.getRotateInstance(Math.toRadians(angle)));
	}
	
	/**
	 * Rotates the current transformation around the given
	 * pivot point by the given angle.
	 * 
	 * @param angle rotation in degrees.
	 * @param pivotX x coordinate of the pivot point.
	 * @param pivotY y coordinate of the pivot point.
	 */
	public void rotate(double angle, double pivotX, double pivotY) {
		transform(AffineTransform.getRotateInstance(Math.toRadians(angle), pivotX, pivotY));
	}
	
	/**
	 * Shears the current transformation matrix by the given
	 * amount.
	 * 
	 * @param x x coordinate of the shear factor
	 * @param y y coordinate of the shear factor
	 */
	public void shear(double x, double y) {
		transform(AffineTransform.getShearInstance(x, y));
	}
	
	
	/**
	 * Gets the total transformation of the current scene node.
	 * 
	 * @return the total transformation.
	 */
	public AffineTransform getTransform() {
		return state.transform;
	}
	
	/**
	 * Gets the square root of the area of a 1 by 1 rectangle 
	 * when transformed into screen coordinates.
	 *  
	 * @return the pixel size.
	 */
	public double getPixelSize() {
		return Math.sqrt(getTransform().getDeterminant());
	}

	/**
	 * Sets the color that is used for stroking paths.
	 * 
	 * @param c stroke color.
	 */
	public void setStrokeColor(ScenicColor c) {
		state.strokeColor = c;
		state.strokeBrush = null;
	}
	
	/**
	 * Sets the brush used for stroking paths.
	 * 
	 * @param brush brush used for stroking.
	 */
	public void setStrokeBrush(SceneNode brush) {
		state.strokeColor = null;
		state.strokeBrush = brush;
	}
	
	/**
	 * Sets the color that is used for filling paths and drawing text.
	 * 
	 * @param c fill color.
	 */
	public void setFillColor(ScenicColor c) {
		state.fillColor = c;
		state.fillBrush = null;
	}

	/**
	 * Sets the brush used for filling paths and drawing text.
	 * 
	 * @param brush brush used for filling.
	 */
	public void setFillBrush(SceneNode brush) {
		state.fillColor = null;
		state.fillBrush = brush;
	}
	
	/**
	 * Sets the current font.
	 * 
	 * @param font the font.
	 */
	public void setFont(Font font) {
		state.font = font;
	}
	
	/**
	 * Sets the text position. The text position defines the start point
	 * for drawing text in logical coordinates.
	 * 
	 * @param x x coordinate of the text position.
	 * @param y y coordinate of the text position.
	 */
	public void setTextPosition(double x, double y) {
		state.textPosition[0] = x;
		state.textPosition[1] = y;
	}
	
	/**
	 * Sets the line width. The line width is given in logical coordinates.
	 * 
	 * @param width the line width.
	 */
	public void setLineWidth(double width) {
		state.lineWidth = width;
	}
	
	/**
	 * Sets the line cap style. The line cap style determines how
	 * the ends of paths are drawn.
	 * 
	 * @param lineCap the line cap style.
	 */
	public void setEndCap(int lineCap) {
		state.lineCap = lineCap;
	}
	
	/**
	 * Sets the line join style. The line join style determines how
	 * joints between lines are drawn.
	 * 
	 * @param lineJoin the line join style.
	 */
	public void setLineJoin(int lineJoin) {
		state.lineJoin = lineJoin;
	}
	
	/**
	 * Sets the miter limit. The miter limit determines the cutoff length
	 * of the spikes when using miter join.
	 * 
	 * @param miterLimit the miter limit.
	 */
	public void setMiterLimit(double miterLimit) {
		state.miterLimit = miterLimit;
	}
	
	/**
	 * Sets the line dash pattern. The dash pattern is defined
	 * using an array which contains the lengths of consecutive
	 * visible and non-visible portions of the dash pattern. The 
	 * lenths are given in logical units. 
	 * 
	 * @param lineDashLengths the line dash pattern.
	 */
	public void setDashArray(float[] lineDashLengths) {
		state.lineDashLengths = lineDashLengths;
	}

	/**
	 * Sets the phase of the line dash pattern. The phase defines the
	 * starting position of the line dash pattern.
	 * 
	 * @param lineDashPhase the line dash phase.
	 */
	public void setDashPhase(double lineDashPhase) {
		state.lineDashPhase = lineDashPhase;
	}
	
	/**
	 * Sets antialiasing. The antialiasing affects all graphics, including
	 * lines, polygons an text.
	 * 
	 * @param aa the antialiasing setting.
	 */
	public void setAntialias(boolean aa) {
		setAntialiasingFilter(aa ? SceneSettings.getDefaultAAFilter() : null);
	}
	
	/**
	 * Sets the filter that is used for antialiasing.
	 * 
	 * @param filter antialiasing filter
	 */
	public void setAntialiasingFilter(Filter filter) {
		SceneSettings settings = new SceneSettings();
		
		settings.setAntialiasingFilter(filter);
		state.scene.add(settings);
		state.scene = settings;
	}
	
	/**
	 * Sets whatever fractional metrics are used to calculate glyph bounds.
	 * This parameter affects only the positioning of characters but
	 * does not change their appearance. This parameter also affects
	 * the bounds returned by getTextLogialBounds.
	 * 
	 * @param b the fractional metrics setting.
	 */
	public void setUsesFractionalFontMetrics(boolean b) {
		state.usesFractionalFontMetrics = b;
	}
	
	/**
	 * Draws the given text using the current font, fill color and
	 * other settings.
	 *  
	 * @param text the text to be drawn.
	 */
	public void drawText(String text) {
		float[] pos;
		GlyphVector gv;			
		FontRenderContext frc = new FontRenderContext(
					state.transform,
					true, state.usesFractionalFontMetrics);
			
		gv = state.font.createGlyphVector(frc, text);
		pos = gv.getGlyphPositions(0, gv.getNumGlyphs(), null);
		
		drawGlyphs(gv.getGlyphCodes(0, gv.getNumGlyphs(), null), pos);	
	}
	
	/**
	 * Draws the given text using the current font, fill color and
	 * other settings. The positions of the characters are given 
	 * as an array of floats. Each position is given as two
	 * numbers the x-position and y-position.
	 *  
	 * @param text the text to be drawn.
	 * @param positions the positions of the characters.
	 */	
	public void drawText(String text, float[] positions) {
		FontRenderContext frc = new FontRenderContext(new AffineTransform(),
			true, true);
		GlyphVector gv = state.font.createGlyphVector(frc, text);
		
		drawGlyphs(gv.getGlyphCodes(0, gv.getNumGlyphs(), null), positions);
	}
	
	/**
	 * Draws the given glyphs using the current font, fill color and
	 * other settings. The positions of the glyphs are given 
	 * as an array of floats. Each position is given as two
	 * numbers the x-position and y-position.
	 *  
	 * @param glyphCodes the glyph codes.
	 * @param positions the positions of the characters.
	 */	
	public void drawGlyphs(int[] glyphCodes, float[] positions) {
		SceneTransform st = new SceneTransform( 
			AffineTransform.getTranslateInstance(state.textPosition[0], state.textPosition[1]));
		TextShape textShape = new TextShape(state.font, glyphCodes, positions);

		state.scene.add(st);
		
		if(state.fillColor != null) {
			textShape.setColor(state.fillColor);
			st.add(textShape);
		} else {
			SceneClip sc = new SceneClip(textShape);
			
			sc.add(state.fillBrush);
			st.add(sc);
		}
	}
	
	/**
	 * Returns the logical bounds of the given text.
	 * 
	 * @param text the text.
	 * @return the logical bounds of the text.
	 */
	public Rectangle2D getTextLogicalBounds(String text) {
		FontRenderContext frc = new FontRenderContext(
				new AffineTransform(),
				true, state.usesFractionalFontMetrics);
		GlyphVector gv = state.font.createGlyphVector(frc, text);
		
		return gv.getLogicalBounds();
	}

	/**
	 * Begins a new subpath from the given position.
	 */
	public void moveTo(double x, double y) {
		path.moveTo(x, y);
	}

	/**
	 * Draws a straight line to the given position.
	 */
	public void lineTo(double x, double y) {
		path.addSegment(new LineSegment(x, y));				
	}

	/**
	 * Draws a quadratic Bezier-curve using the given
	 * control points. The first control point is the current
	 * position.
	 * 
	 * @param p1 the second control point.
	 * @param p2 the third control point.
	 */
	public void curveTo(Point2D p1, Point2D p2) {
		path.curveTo(p1, p2);
	}
	
	/**
	 * Draws a cubic Bezier-curve using the given
	 * control points. The first control point is the current
	 * position.
	 * 
	 * @param p1 the second control point.
	 * @param p2 the third control point.
	 * @param p3 the fourth control point.
	 */
	public void curveTo(Point2D p1, Point2D p2, Point2D p3) {
		path.curveTo(p1, p2, p3);
	}
	
	/**
	 * Draws an elliptic arc starting from the current position.
	 * 
	 * @param xRadius the x radius of the ellipse.
	 * @param yRadius the y radius of the ellipse.
	 * @param rotation the rotation of the ellipse in degrees.
	 * @param startAngle the start angle of the arc in degrees.
	 * @param stopAngle the stop angle of the arc in degrees.
	 */
	public void arcTo(double xRadius, double yRadius, double rotation, 
			double startAngle, double stopAngle)
	{
		path.arcTo(xRadius, yRadius, rotation, startAngle, stopAngle);
	}
	
	/**
	 * Draws an elliptic arc starting from the given position.
	 * 
	 * @param x the x-coordinate of the starting point of the arc.
	 * @param y the y-coordinate of the starting point of the arc.
	 * @param xRadius the x radius of the ellipse.
	 * @param yRadius the y radius of the ellipse.
	 * @param rotation the rotation of the ellipse in degrees.
	 * @param startAngle the start angle of the arc in degrees.
	 * @param stopAngle the stop angle of the arc in degrees.
	 */
	public void arc(double x, double y, double xRadius, 
			double yRadius, double rotation, 
			double startAngle, double stopAngle)
	{
		path.arc(x, y, xRadius, yRadius, rotation, startAngle, stopAngle);
	}
	
	/**
	 * Draws a rectangle.
	 * 
	 * @param x the left side of the rectangle.
	 * @param y the top of the rectangle.
	 * @param width the width of the rectangle.
	 * @param height the height of the rectangle.
	 */
	public void rectangle(double x, double y, double width, double height) {
		path.rectangle(x, y, width, height);
	}

	/**
	 * Draws a rectangle with rounded edges. 
	 * 
	 * @param x the left side of the rectangle.
	 * @param y the top of the rectangle.
	 * @param width the width of the rectangle.
	 * @param height the height of the rectangle.
	 * @param radius the radius of the rounded edges.
	 */
	public void roundedRectangle(double x, double y, double width, double height, 
			double radius) {
		path.roundedRectangle(x, y, width, height, radius);		
	}
	
	/**
	 * Closes the current subpath.
	 */
	public void close() {
		path.close();
	}

	private SceneNode createNode(SceneShape shape, ScenicColor color, SceneNode brush) {
		if(color != null) {
			shape.setColor(color);
			
			return shape;
		} else {
			SceneClip sc = new SceneClip(shape);
			
			sc.add(brush);
			
			return sc;
		}
	}
	
	/**
	 * Strokes the current path.
	 */
	public void stroke()
	{
		StrokedPath r = new StrokedPath(path.createPath());
		
		r.setLineWidth(state.lineWidth);
		r.setEndCap(state.lineCap);
		r.setLineJoin(state.lineJoin);
		r.setMiterLimit(state.miterLimit);
		r.setDashArray(state.lineDashLengths);
		r.setDashPhase(state.lineDashPhase);

		state.scene.add(createNode(r, state.strokeColor, state.strokeBrush));
		
		path = new PathBuilder();
	}


	/**
	 * Fills the current path.
	 */
	public void fill()
	{
		FilledPath shape = new FilledPath(path.createPath());
		
		shape.setFillRule(state.fillRule);
		state.scene.add(createNode(shape, state.fillColor, state.fillBrush));

		path = new PathBuilder();	
	}

	/**
	 * Fills and strokes the current path.
	 */
	public void fillAndStroke()
	{
		Path p = path.createPath();
		FilledPath shape = new FilledPath(p);
			
		shape.setFillRule(state.fillRule);
		state.scene.add(createNode(shape, state.fillColor, state.fillBrush));
			
		StrokedPath r2 = new StrokedPath(p);

		r2.setLineWidth(state.lineWidth);
		r2.setEndCap(state.lineCap);
		r2.setLineJoin(state.lineJoin);
		r2.setMiterLimit(state.miterLimit);

		state.scene.add(createNode(r2, state.strokeColor, state.strokeBrush));

		path = new PathBuilder();		
	}

	/**
	 * Clips to the area inside the current path.
	 */
	public void clip() {
		FilledPath shape = new FilledPath(path.createPath());
		SceneClip clip = new SceneClip(shape);

		state.scene.add(clip);
		state.scene = clip;
		
		path = new PathBuilder();	
	}

	/**
	 * Draws an image.
	 * 
	 * @param img the image.
	 */
	public void drawImage(ScenicImage img) {
		state.scene.add(new SceneImage(img));		
	}
	
	/**
	 * Draws the given part of an image.
	 * 
	 * @param img the image.
	 * @param sourceRect the part of the image that is displayed.
	 */
	public void drawImage(ScenicImage img, Rectangle2D sourceRect) {
		state.scene.add(new SceneImage(img, sourceRect));		
	}
	
	/**
	 * Gets a Path object that contains the current path.
	 * 
	 * @return the current path.
	 */
	public Path getPath() {
		return path.createPath();
	}
	
	/**
	 * Adds a scene node to the scene.
	 * 
	 * @param node the scene node top be added.
	 */
	public void add(SceneNode node) {
		state.scene.add(node);
	}
	
	/**
	 * Clips graphics using the given clip scene.
	 * 
	 * @param clipScene the scene node that is used for clipping.
	 */
	public void clipTo(SceneNode clipScene) {
		SceneClip clip = new SceneClip(clipScene);

		state.scene.add(clip);
		state.scene = clip;
	}
}
